package com.itheima.pinda.log.aspect;


import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.itheima.pinda.auth.client.utils.JwtTokenClientUtils;
import com.itheima.pinda.auth.utils.JwtUserInfo;
import com.itheima.pinda.auth.utils.Token;
import com.itheima.pinda.base.R;
import com.itheima.pinda.log.entity.LoginLogDTO;
import com.itheima.pinda.log.event.LoginEvent;
import com.itheima.pinda.utils.AddressUtils;
import eu.bitwalker.useragentutils.Browser;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.util.Objects;

/**
 * 登陆日志使用spring event异步入库
 * <p>
 * 注意点: 这里的切面都没加上 @Component注解, 也就说没有注入到容器中
 * 1.可以在LogAutoConfiguration配置可注入
 * 2.在此类加上@Component注解即可注入
 */
@Slf4j
@Aspect
public class LoginLogAspect {

    /**
     * 事件发布是由ApplicationContext对象管控的，我们发布事件前需要注入ApplicationContext对象调用publishEvent方法完成事件发布
     **/
    @Resource
    private ApplicationContext applicationContext;

    @Resource
    private JwtTokenClientUtils jwtTokenClientUtils;

    /***
     * 定义controller切入点拦截规则，拦截LoginLog注解的方法
     */
    @Pointcut("@annotation(com.itheima.pinda.log.annotation.LoginLog)")
    public void loginLogAspect() {

    }


    // @SneakyThrows(BizException.class)
    // @SneakyThrows // 这个能处理 point.proceed()方法执行抛出的异常
    @Around(value = "loginLogAspect()")
    public Object around(ProceedingJoinPoint point) {
        // 操作ip
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String ip = ServletUtil.getClientIP(request);

        MethodSignature methodSignature = (MethodSignature) point.getSignature();

        // 登录账号
        String account = null;
        // 参数
        Object[] args = point.getArgs();
        if (!Objects.isNull(args)) {
            Object target = args[0];
            // 获取登录账号名称
            account = this.getLoginName(target);
        }

        // 请求头
        String ua = StrUtil.sub(request.getHeader("user-agent"), 0, 500);
        // 浏览器名称
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        Browser browser = userAgent.getBrowser();
        String browserName = browser.getName();
        // 浏览器版本
        String version = browser.getVersion(request.getHeader("User-Agent")).getVersion();
        // 操作系统
        String os = userAgent.getOperatingSystem().getName();
        // 登录地点
        String address = AddressUtils.getRealAddressByIP(ip);

        // 发送异步日志事件, 执行target标识的方法
        Object obj;
        try {
            obj = point.proceed();
        } catch (Throwable th) {
            // todo 学长这里异常处理 抓取BpException, 对应一个策略类处理异常, 一般都是save DB
            log.error(" {} 方法执行异常", methodSignature.getName());
            throw new RuntimeException(th.getMessage(), th);
        }
        // todo 学长这里设计的是从token取出这些信息
        //   如何获取token呢 ? 引入 pd-tools-jwt模块, 开启jwtTokenClientUtils, 这样就可以解析token
        // 获取token
        R r = (R) obj;
        String token = this.getToken(r);
        // 解析Token中的 JwtUserInfo
        JwtUserInfo userInfo = jwtTokenClientUtils.getUserInfo(token);
        // 登录id
        Long userId = userInfo.getUserId();
        // 登录姓名
        String name = userInfo.getName();

        LoginLogDTO loginLogDTO = LoginLogDTO.builder()
                .requestIp(ip)
                .userId(userId)
                .userName(name)
                .account(account)
                .description("")
                .loginDate(LocalDate.now())
                .ua(ua)
                .browser(browserName)
                .browserVersion(version)
                .operatingSystem(os)
                .location(address)
                .build();

        log.info("================== loginLogDTO: {}", loginLogDTO);

        applicationContext.publishEvent(new LoginEvent(loginLogDTO));
        return obj;
    }


    /**
     * 利用反射获取登录成功后的token
     *
     * @param target 登陆成功的返回结果
     * @return token
     */
    private String getToken(Object target) {
        try {
            // 调用属性的get方法
            Method method = target.getClass().getMethod("getData");
            Object loginDTO = method.invoke(target);
            if (loginDTO != null) {
                Method tokenMethod = loginDTO.getClass().getMethod("getToken");
                Object invoke = tokenMethod.invoke(loginDTO);
                if (invoke != null) {
                    Token token = (Token) invoke;
                    return token.getToken();
                }
            }
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
            log.error("{} 类没有{} 方法", target.getClass(), "getData");
            throw new RuntimeException(ex.getMessage(), ex);
        }
        return null;
    }

    /**
     * 利用反射获取登录方法中的账号字段值
     *
     * @param target 登录方法入参
     * @return 账号
     */
    private String getLoginName(Object target) {
        try {
            // 调用属性的get方法
            Method method = target.getClass().getMethod("getAccount");
            Object value = method.invoke(target);
            if (value != null) {
                return (String) value;
            }
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
            log.error("{} 类没有{} 方法", target.getClass(), "getAccount");
            throw new RuntimeException(ex.getMessage(), ex);
        }
        return null;
    }

}
